package displayboardinfo.services;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;

import org.hibernate.Hibernate;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

import com.sun.jersey.api.client.ClientResponse.Status;

import displayboardinfo.entities.Admission;
import displayboardinfo.entities.RegisteredForUpdates;
import displayboardinfo.entities.Room;
import displayboardinfo.entities.Visit;
import displayboardinfo.messages.FinishVisitReq;
import displayboardinfo.messages.ListVisitsReq;
import displayboardinfo.messages.ListVisitsRes;
import displayboardinfo.utils.LogicalException;
import displayboardinfo.utils.Utils;

/**
 * Klasa odpowiedzialna za wystawianie usług związanych z obsługą wizyt.
 * 
 * @author Artur Jaworski
 */
@Path("/visits")
public class VisitsServices {
	
	/**
	 * Usługa listowania wizyt w konkretnym gabinecie konkretnego dnia.
	 * Usługa dostępna jest na serwerze pod adresem /visits/list używając metody POST.
	 * 
	 * @return lista pacjentów w systemie.
	 */
	@SuppressWarnings({ "rawtypes", "deprecation" })
	@Path("/list")
	@POST
	@Produces(MediaType.APPLICATION_XML)
	public ListVisitsRes list(JAXBElement<ListVisitsReq> listVisitsReqJaxb) {
		SessionFactory sf = new Configuration().configure()
				.buildSessionFactory();
		Session session = sf.openSession();
		ListVisitsReq request = (ListVisitsReq) listVisitsReqJaxb.getValue();
		ListVisitsRes response = new ListVisitsRes();

		Calendar cal = Calendar.getInstance();
		cal.setTime(request.getDate());
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		Date startDate = cal.getTime();
		cal.add(Calendar.DAY_OF_YEAR, 1);
		Date endDate = cal.getTime();

		Query query = session
				.createQuery(
						"from Visit v where v.room = :room and v.date >= :start and v.date < :end order by v.estimatedEntry asc")
				.setParameter("room", request.getRoom())
				.setDate("start", startDate).setDate("end", endDate);

		for (Iterator it = query.iterate(); it.hasNext();) {
			Object item = it.next();

			Visit visit = (Visit) item;
			response.addVisit(visit);
		}

		session.close();
		sf.close();
		return response;

	}

	/**
	 * Usługa dodania nowej wizyty.
	 * Usługa dostępna jest na serwerze pod adresem /visits/add używając metody POST.
	 * Usługa zwraca kod 200 [OK] gdy wywołanie się powiedzie lub kod 406 [NOT ACCEPTABLE] gdy wystąpi błąd (wizyta koliduje z inną wizytą, gabinet nie przyjmuje w wyznaczonym czasie, błąd serwera).
	 * Przy wystąpieniu błędu do odpowiedzi dołączany jest krótki opis przyczyny błędu.
	 * Po wywołaniu usługi następuje aktualizacja klientów zarejestrowanych na aktualizacje gabinetu.
	 * 
	 * @param visitJaxb obiekt reprezentujący dodawaną wizytę, w formie koperty XML
	 * @return obiekt stworzonej wizyty z uzupełnionym ID nadanym przez bazę danych
	 */
	@SuppressWarnings("deprecation")
	@Path("/add")
	@POST
	@Consumes(MediaType.APPLICATION_XML)
	public Visit add(JAXBElement<Visit> visitJaxb) {
		Visit visit = visitJaxb.getValue();
		SessionFactory sf = new Configuration().configure()
				.buildSessionFactory();
		Session session = sf.openSession();
		Transaction transaction = session.beginTransaction();

		try {
			// Check overlapping visits
			ListVisitsReq listRequest = new ListVisitsReq();
			listRequest.setDate(visit.getDate());
			listRequest.setRoom(visit.getRoom());

			JAXBElement<ListVisitsReq> listRequestJaxb = new JAXBElement<ListVisitsReq>(
					new QName(""), ListVisitsReq.class, null, listRequest);

			ListVisitsRes listResponse = list(listRequestJaxb);

			// System.out.println(listResponse.getVisits().size() +
			// " visits found");

			for (Visit v : listResponse.getVisits()) {
				if (visit.isOverlapping(v))
					throw new LogicalException("Wizyta koliduje z inną wizytą");
			}
			
			boolean found = false;
			for (Admission a : Utils.listAdmissions(visit.getRoom())){
				if (a.isInDate(visit.getDate())){
					found = true;
					break;
				}
			}

			if (!found){
				throw new LogicalException("Gabinet nie przyjmuje w wyznaczonym terminie");
			}
			
			visit.setEstimatedEntry(visit.getDate());
			session.save(visit);
			transaction.commit();
			
			for (RegisteredForUpdates reg : Utils.listRegistered(visit.getRoom())){
				reg.update();
			}
			
			return visit;
		} catch (LogicalException e) {
			transaction.rollback();
			throw new WebApplicationException(Response
					.status(Status.NOT_ACCEPTABLE).entity(e.getMessage())
					.type(MediaType.TEXT_PLAIN).build());
		} catch (Exception e) {
			transaction.rollback();
			throw new WebApplicationException(Response
					.status(Status.NOT_ACCEPTABLE).entity("Nieznany błąd")
					.type(MediaType.TEXT_PLAIN).build());
		} finally {
			session.close();
			sf.close();
		}

	}

	/**
	 * Usługa modyfikacji istniejącej wizyty.
	 * Usługa dostępna jest na serwerze pod adresem /visits/modify używając metody POST.
	 * Usługa zwraca kod 200 [OK] gdy wywołanie się powiedzie lub kod 406 [NOT ACCEPTABLE] gdy wystąpi błąd (wizyta koliduje z inną wizytą, gabinet nie przyjmuje w określonym czasie, błąd serwera).
	 * Przy wystąpieniu błędu do odpowiedzi dołączany jest krótki opis przyczyny błędu.
	 * Po wywołaniu usługi następuje aktualizacja klientów zarejestrowanych na aktualizacje gabinetu (zarówno starego jak i nowego jeżeli to gabinet został zmieniony).
	 * 
	 * @param visitJaxb obiekt reprezentujący modyfikowaną wizytę, w formie koperty XML
	 * @return pusta odpowiedź, zawierająca kod i komunikat o błędzie w przypadku niepowodzenia
	 */
	@SuppressWarnings({ "deprecation", "rawtypes" })
	@Path("/modify")
	@POST
	@Consumes(MediaType.APPLICATION_XML)
	public Response modify(JAXBElement<Visit> visitJaxb) {
		Visit visit = visitJaxb.getValue();
		Room oldRoom = null;

		SessionFactory sf = new Configuration().configure()
				.buildSessionFactory();
		Session session = sf.openSession();

		Transaction transaction = session.beginTransaction();
		try {
			// Check overlapping visits
			ListVisitsReq listRequest = new ListVisitsReq();
			listRequest.setDate(visit.getDate());
			listRequest.setRoom(visit.getRoom());

			JAXBElement<ListVisitsReq> listRequestJaxb = new JAXBElement<ListVisitsReq>(
					new QName(""), ListVisitsReq.class, null, listRequest);

			ListVisitsRes listResponse = list(listRequestJaxb);

			// System.out.println(listResponse.getVisits().size() +
			// " visits found");

			for (Visit v : listResponse.getVisits()) {
				if (visit.isOverlapping(v))
					throw new LogicalException("Wizyta koliduje z inną wizytą");
			}
			
			Query query = session.createQuery("from Visit v where v.id = :id").setParameter("id", visit.getId());
			for (Iterator it = query.iterate(); it.hasNext();) {
				Object item = it.next();

				Visit tmp = (Visit) item;
				Hibernate.initialize(tmp.getRoom());
				
				oldRoom = tmp.getRoom();
			}
			
			boolean found = false;
			for (Admission a : Utils.listAdmissions(visit.getRoom())){
				if (a.isInDate(visit.getDate())){
					found = true;
					break;
				}
			}

			if (!found){
				throw new LogicalException("Gabinet nie przyjmuje w wyznaczonym terminie");
			}
			
			visit.setEstimatedEntry(visit.getDate());
			session.update(visit);
			transaction.commit();
			
			for (RegisteredForUpdates reg : Utils.listRegistered(visit.getRoom())){
				reg.update();
			}
			
			if (visit.getRoom().getId() != oldRoom.getId()){
				for (RegisteredForUpdates reg : Utils.listRegistered(oldRoom)){
					reg.update();
				}	
			}
			
			return Response.ok().build();
		} catch (LogicalException e) {
			transaction.rollback();
			throw new WebApplicationException(Response
					.status(Status.NOT_ACCEPTABLE).entity(e.getMessage())
					.type(MediaType.TEXT_PLAIN).build());
		} catch (Exception e) {
			transaction.rollback();
			throw new WebApplicationException(Response
					.status(Status.NOT_ACCEPTABLE).entity("Nieznany błąd")
					.type(MediaType.TEXT_PLAIN).build());
		} finally {
			session.close();
			sf.close();
		}

	}

	/**
	 * Usługa usunięcia istniejącej wizyty.
	 * Usługa dostępna jest na serwerze pod adresem /visits/delete używając metody POST.
	 * Usługa zwraca kod 200 [OK] gdy wywołanie się powiedzie lub kod 406 [NOT ACCEPTABLE] gdy wystąpi błąd (nieistniejąca wizyta, błąd serwera).
	 * Przy wystąpieniu błędu do odpowiedzi dołączany jest krótki opis przyczyny błędu.
	 * Po wywołaniu usługi następuje aktualizacja klientów zarejestrowanych na aktualizacje gabinetu.
	 * 
	 * @param visitJaxb obiekt reprezentujący wizytę usuwaną z systemu, w formie koperty XML
	 * @return pusta odpowiedź, zawierająca kod i komunikat o błędzie w przypadku niepowodzenia
	 */
	@SuppressWarnings("deprecation")
	@Path("/delete")
	@POST
	@Consumes(MediaType.APPLICATION_XML)
	public Response delete(JAXBElement<Visit> visitJaxb) {
		Visit visit = visitJaxb.getValue();

		SessionFactory sf = new Configuration().configure()
				.buildSessionFactory();
		Session session = sf.openSession();

		Transaction transaction = session.beginTransaction();
		try {
			Query query = session.createQuery(
					"delete from Visit v where v.id = :id").setParameter("id",
					visit.getId());
			query.executeUpdate();
			transaction.commit();
			
			for (RegisteredForUpdates reg : Utils.listRegistered(visit.getRoom())){
				reg.update();
			}
			
			return Response.ok().build();
		} catch (Exception e) {
			transaction.rollback();
			throw new WebApplicationException(Response
					.status(Status.NOT_ACCEPTABLE).entity("Nieznany błąd")
					.type(MediaType.TEXT_PLAIN).build());
		} finally {
			session.close();
			sf.close();
		}

	}

	/**
	 * Usługa zakończenia wizyty.
	 * Usługa dostępna jest na serwerze pod adresem /visits/finish używając metody POST.
	 * Usługa zwraca kod 200 [OK] gdy wywołanie się powiedzie lub kod 406 [NOT ACCEPTABLE] gdy wystąpi błąd (nieistniejąca wizyta, błąd serwera).
	 * Przy wystąpieniu błędu do odpowiedzi dołączany jest krótki opis przyczyny błędu.
	 * Po wywołaniu usługi następuje aktualizacja klientów zarejestrowanych na aktualizacje gabinetu.
	 * Usługa sprawdza czy występuje opóźnienie lub przyspieszenie w przyjmowaniu pacjentów i odpowiednio modyfikuje pola estimatedEntry w obiektach wizyt.
	 * 
	 * @param visitJaxb obiekt reprezentujący kończoną wizytę, w formie koperty XML
	 * @return pusta odpowiedź, zawierająca kod i komunikat o błędzie w przypadku niepowodzenia
	 */
	@SuppressWarnings("deprecation")
	@Path("/finish")
	@POST
	@Consumes(MediaType.APPLICATION_XML)
	public Response finish(JAXBElement<FinishVisitReq> visitJaxb) {
		FinishVisitReq finishVisit = visitJaxb.getValue();

		SessionFactory sf = new Configuration().configure()
				.buildSessionFactory();
		Session session = sf.openSession();
		Transaction transaction = session.beginTransaction();
		try {
			if (!finishVisit.isOutOfQueue()) {
				ListVisitsReq listRequest = new ListVisitsReq();
				listRequest.setDate(finishVisit.getVisit().getDate());
				listRequest.setRoom(finishVisit.getVisit().getRoom());

				JAXBElement<ListVisitsReq> listRequestJaxb = new JAXBElement<ListVisitsReq>(
						new QName(""), ListVisitsReq.class, null, listRequest);

				ListVisitsRes listResponse = list(listRequestJaxb);

				// obliczenie opoznienia/przyspieszenia
				Calendar cal = Calendar.getInstance();
				Date currentDate = new Date();

				Date visitDate = finishVisit.getVisit().getDate();

				if (visitDate.getDate() == currentDate.getDate()
						&& visitDate.getMonth() == currentDate.getMonth()
						&& visitDate.getYear() == currentDate.getYear()) {

					// DEBUGING INFO
					// SimpleDateFormat format = new
					// SimpleDateFormat("dd/MM/yyyy HH:mm");
					// currentDate = format.parse("19/03/2012 12:45");
					// System.out.println(format.format(currentDate) + " " +
					// format.format(visitDate));

					long diffTime = currentDate.getTime()
							- (visitDate.getTime()
									+ finishVisit.getVisit().getDuration()
											.getTime() + cal
										.get(Calendar.ZONE_OFFSET));

					// propagacja na kolejne wizyty podpiete do gabinetu
					ArrayList<Visit> v = listResponse.getVisits();
					System.out.println("Visits: " + v.size());
					for (int i = 0; i < v.size(); i++) {
						// calculate margin between visits
						if (i != 0) {
							long margin = v.get(i).getDate().getTime()
									- (v.get(i - 1).getDate().getTime()
											+ v.get(i - 1).getDuration()
													.getTime() + cal
												.get(Calendar.ZONE_OFFSET));
							diffTime -= margin;
							diffTime = Math.max(diffTime, 0);
							diffTime -= diffTime % 60000;
						}

						if (v.get(i).getId() != finishVisit.getVisit().getId()) {
							v.get(i)
									.getEstimatedEntry()
									.setTime(
											v.get(i).getDate().getTime()
													+ diffTime);
							System.out.println(v.get(i));
							session.update(v.get(i));
						}
					}
				}
			}

			Query query = session.createQuery(
					"delete from Visit v where v.id = :id").setParameter("id",
					finishVisit.getVisit().getId());
			query.executeUpdate();

			transaction.commit();
			
			// aktualizacja wszystkich wyswietlaczy podpietych do gabinetu
			System.out.println("Registered:");
			for (RegisteredForUpdates reg : Utils.listRegistered(finishVisit.getVisit().getRoom())){
				reg.update();
			}
			
			return Response.ok().build();
		} catch (Exception e) {
			e.printStackTrace();
			transaction.rollback();
			throw new WebApplicationException(Response
					.status(Status.NOT_ACCEPTABLE).entity("Nieznany błąd")
					.type(MediaType.TEXT_PLAIN).build());
		} finally {
			session.close();
			sf.close();
		}
	}

}
